-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Load test classes with runtime classloader #34681
base: main
Are you sure you want to change the base?
Conversation
f5a756d
to
f661328
Compare
2c4ddb4
to
8f88f8a
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
aba5d5c
to
68e6194
Compare
This comment has been minimized.
This comment has been minimized.
Still watching this one. I've created a minimal not-working-example to see if this fixes it. :) |
Still going on it ... :) The profile support in normal mode turned out to be a bit thorny, so looking at that now. I haven't checked for a while, but at one point I had reproducers for three of the test-classloading-related defects. One was passing, but two (annoyingly) were failing. If your reproducer is shareable, I'm happy to take it and include it in what I'm checking, or to add it into the test suites, if it's a gap in what we're testing now. (It'd be worth checking what I've added in #35124 to see if one of those covers your scenario, too.) |
4307712
to
181a730
Compare
This comment has been minimized.
This comment has been minimized.
d69004e
to
9db70c2
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
8a702df
to
7a74a0d
Compare
This comment has been minimized.
This comment has been minimized.
7a74a0d
to
ceed7b9
Compare
This comment has been minimized.
This comment has been minimized.
5d9fac9
to
1bdd946
Compare
This comment has been minimized.
This comment has been minimized.
1bdd946
to
7aa5165
Compare
This comment has been minimized.
This comment has been minimized.
@@ -309,12 +311,21 @@ public Supplier<DependencyInfoProvider> getDependencyInfoProvider() { | |||
return depInfoProvider; | |||
} | |||
|
|||
// TODO where is the cleanest place for this to live? We don't want code which has been given classes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @aloubyansky
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ApplicationModel.getAppArtifact().getWorkspaceModule().getTestSources()
and then either getOutputTree()
or getSourceDirs().getOutputDir()
is meant to be that. But we need to see how/where it fits in this case.
@@ -440,6 +448,11 @@ public void close() { | |||
augmentationElements.clear(); | |||
} | |||
|
|||
// TODO delete this? the model doesn't really work? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't really work, in what sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. That comment is so old I wasn't entirely sure myself, when I was reviewing the TODOs. :)
I think what it's referring to is that at an early stage of development I had good ideas about re-using more of the curated application + base classloaders between different applications. That is, if I decided we needed to restart, instead of throwing everything out, I'd do a light tidy and then re-use the lower levels of the classloader stack. That might still be a good thing to do, and it would reduce the memory footprint of the new 'load everything upfront' pattern. If we can do restarts for continuous testing we should be able to achieve some reuse in normal mode testing. But I never made it work.
So I think that tidy()
method is a vestige of my attempts to re-use stuff. The question is whether it's now totally pointless because cleanup happens elsewhere, or whether it's still a useful part of the cleanup that we do between restarts. I'll inspect the code and try and work it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙏🏽
@geoand , FYI I've just spotted that these changes would break the JBeret Ecosystem CI. I think I know what the fix is, but I'll need to make an update. Hopefully it won't be a big update, just a tweak to the quarkus test detection logic to catch whatever edge case is breaking it in the JBeret tests. |
Thanks for the heads up! |
7aa5165
to
32496bf
Compare
* would allow us to swap the thread context classloader. | ||
* Since we can't intercept with a JUnit hook, we hijack from inside the classloader. | ||
* <p> | ||
* We need to load all our test classes in one go, during the discovery phase, before we start the applications. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to load all our test classes in one go, during the discovery phase
Is this a current JUnit limitation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, to the best of my knowledge. I banged my head against it for a while, but I don't see a solution that's not writing a new engine, or doing a pre-test-reload. Pre-test-reload sounds appealing, but it's basically the existing approach. It means JUnit doesn't see our modified test code, because if we reload post-discovery, it's too late for some JUnit functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it thanks! Any pointers at where I can place debug breakpoints to see this in action?
I would like to understand the current limitations so after we get this in, we can go back to the JUnit 5 people with a details on what we use so far and why it's not enough.
This comment has been minimized.
This comment has been minimized.
TestSupport.instance().get()::isDisplayTestOutput); | ||
TestSupport.instance() | ||
.get()::isDisplayTestOutput); | ||
// TODO do we want to do this setting of the TCCL? I think it just makes problems? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific hints about the possible problems?
32496bf
to
053442c
Compare
} else if (resource.getProtocol().equals("quarkus")) { | ||
// This is loaded with a quarkus classloader, so we can ask it directly | ||
QuarkusClassLoader qcl = (QuarkusClassLoader) testClass.getClassLoader(); | ||
return qcl.getCuratedApplication().getQuarkusBootstrap().getTestClassesLocation(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we (theoretically) could try qcl.getCuratedApplication().getApplicationModel().getAppArtifact().getWorkspaceModule().getTestSources().getOutputDir()
if (curatedApplication == null) { | ||
curatedApplication = makeCuratedApplication(requiredTestClass, displayName, isContinuousTesting, shutdownTasks); | ||
} | ||
Path testClassLocation = getTestClassLocationIncludingPossibilityOfGradleModel(requiredTestClass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like testClassLocation
should already be available from the curatedApplication
here? Could they be different here? If they are, isn't it an issue?
Status for workflow
|
Bugs fixed by this PR
@Nested
tests #45349@QuarkusTest
method parameter fails withClassNotFoundException
#42006Bugs created by this PR (doh!)
Outstanding issues/breaking changes (input to release notes)
@TestProfile
on@Nested
tests are not supported anymore (fixes QuarkusTest: consider removing the test profile support for@Nested
tests #45349).surefire.rerunFailingTestsCount
option does not work in the case where there are test profiles or resources, unless the test order falls such that the failing test uses the last profile/resource (Maven surefire.rerunFailingTestsCount option does not work in the case where there are test profiles or resources, unless the test order falls such that the failing test uses the last profile/resource #46048, harder to fix)What problem is this solving?
We see a lot of problems caused by the fact that we load test classes with the deployment classloader, and then intercept the execution and reload the classes with the runtime classloader. Although the new test is loaded with the runtime classloader, its arguments are still loaded with the system classloader. To work around that we sometimes need to clone the arguments by serializing and de-serializing. This was always brittle and no longer worked at all on Java 17+ (until #40601 fixed that). We also see issues because parts of the test infrastructure see the 'wrong' instance of the class. See, for example, quarkiverse/quarkus-pact#73 and #22611.
We have several feature raised against the JUnit team to allow us more control over classloading. The first of this features was introduced in JUnit 5.10, and allows an interceptor to be registered before any tests are launched. This interceptor can set a thread context classloader, which is then used by JUnit to load tests.
My experiments with this feature were thoroughly disappointing. It turns out, setting a TCCL early in the test lifecycle doesn't really help us, because we overwrite our 'early' TCCL with other TCCLs later in the test lifecycle. The following diagram shows some of the places we set the TCCL.
Source: https://excalidraw.com/#json=HFPHIKx8wv0iiyXgNhAzw,8IlEmPcMRvm9pfCGShdClQ
What if we just used one of the existing interception points to set the 'right' classloader, before tests are loaded? If the tests were loaded with our preferred classloader, we wouldn’t need to intercept the factory. Loading the tests with the runtime classloader needs us to move some of our app initialisation earlier in the lifecycle, but I don't think there's any fundamental barrier to this. (We would have had to do this with a solution based on the new JUnit Launcher Interceptor anyway.)
The logic for starting Quarkus needs to be in the test discovery phase, rather than in the extension. This allows us to create the runtime classloader before the test is loaded. The JUnitTest runner already knows about the Quarkus Extension, so it’s only a small extra bit of knowledge to do some of the startup actions.
This only gets us part of the way, though. @stuartwdouglas raised the point that if we have to set only a single classloader, that's not very flexible, because we have a runtime classloader for each test profile. A Quarkus test run doesn't just use one classloader, it uses several. Every resource/unique profile triggers an app relaunch, which means a new classloader. What I've done to handle this is create a FacadeClassLoader. It takes the classloading requests, and then either routes them on to the quarkus application (for vanilla @QuarkusTests), or, if there's a profile/resource, it makes a new app + classloader and sends the request to that.
What we used to before was load a throwaway copy of the the test, pass it to JUnit discovery, let JUnit launch it, and then intercept the execution, figure out what profiles+resources the test declares, create a quarkus app with that information, start the quarkus app, reload the test with the runtime classloader of the quarkus app (and clone its parameters), and execute the test.
The new model is load a throwaway copy of the the test, figure out what profiles+resources the test declares, create a quarkus app with that information, reload the test with the runtime classloader of the quarkus app, pass the ‘right’ class to JUnit discovery, let JUnit launch it, and then intercept the execution, start the quarkus app, and execute the test.
One fundamental limitation of "load tests with the classloader used to execute them" is that a single test cannot run with multiple classloaders, which means it cannot support multiple profiles. We know some people do use this feature, but we also know there have been suggestions that we drop support for it, since it is complex to support (#45349). There is an easy workaround, which is to use one test per profile.
Thoughts on serialization and cloning
A big initial goal of this PR was to get rid of the xstream serialization, since it didn't work on Java 17+. #40601 fixes this issue by switching to use the JBoss marshaller for serialization. Does that mean this work item isn't needed any more? No, although it does mean its benefits are smaller. Here's why it's still useful:
@TestTemplate
) does not see Quarkus bytecode transformations done by extensionssun.misc.unsafe
, butunsafe
is shrinking. It seems certain the JDK team will have to come up with some solution and API to open up access for serializers, but the final design could have security implications (perhaps opening up access in a blanket way), or performance implications (reflection fun), or user experience implications (a need to manually set flags such as--enable-serialization
?). If we can avoid serialization, we avoid all that.Todo before this merges
Todo after this merges
QuarkusTestExtension